/* *
 *
 *  Client side exporting module
 *
 *  (c) 2015 Torstein Honsi / Oystein Moseng
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
/* global MSBlobBuilder */
import Highcharts from "../parts/Globals.js";
import U from "../parts/Utilities.js";
var extend = U.extend;
import "../parts/Chart.js";
import "../parts/Options.js";
import "../mixins/download-url.js";
var addEvent = Highcharts.addEvent,
  merge = Highcharts.merge,
  win = Highcharts.win,
  nav = win.navigator,
  doc = win.document,
  domurl = win.URL || win.webkitURL || win,
  isMSBrowser = /Edge\/|Trident\/|MSIE /.test(nav.userAgent),
  // Milliseconds to defer image load event handlers to offset IE bug
  loadEventDeferDelay = isMSBrowser ? 150 : 0;
// Dummy object so we can reuse our canvas-tools.js without errors
Highcharts.CanVGRenderer = {};
/* eslint-disable valid-jsdoc */
/**
 * Downloads a script and executes a callback when done.
 *
 * @private
 * @function getScript
 * @param {string} scriptLocation
 * @param {Function} callback
 * @return {void}
 */
function getScript(scriptLocation, callback) {
  var head = doc.getElementsByTagName("head")[0],
    script = doc.createElement("script");
  script.type = "text/javascript";
  script.src = scriptLocation;
  script.onload = callback;
  script.onerror = function () {
    Highcharts.error("Error loading script " + scriptLocation);
  };
  head.appendChild(script);
}
/**
 * Get blob URL from SVG code. Falls back to normal data URI.
 *
 * @private
 * @function Highcharts.svgToDataURL
 * @param {string} svg
 * @return {string}
 */
Highcharts.svgToDataUrl = function (svg) {
  // Webkit and not chrome
  var webKit =
    nav.userAgent.indexOf("WebKit") > -1 && nav.userAgent.indexOf("Chrome") < 0;
  try {
    // Safari requires data URI since it doesn't allow navigation to blob
    // URLs. Firefox has an issue with Blobs and internal references,
    // leading to gradients not working using Blobs (#4550)
    if (!webKit && nav.userAgent.toLowerCase().indexOf("firefox") < 0) {
      return domurl.createObjectURL(
        new win.Blob([svg], {
          type: "image/svg+xml;charset-utf-16",
        })
      );
    }
  } catch (e) {
    // Ignore
  }
  return "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
};
/**
 * Get data:URL from image URL. Pass in callbacks to handle results.
 *
 * @private
 * @function Highcharts.imageToDataUrl
 *
 * @param {string} imageURL
 *
 * @param {string} imageType
 *
 * @param {*} callbackArgs
 *        callbackArgs is used only by callbacks.
 *
 * @param {number} scale
 *
 * @param {Function} successCallback
 *        Receives four arguments: imageURL, imageType, callbackArgs, and scale.
 *
 * @param {Function} taintedCallback
 *        Receives four arguments: imageURL, imageType, callbackArgs, and scale.
 *
 * @param {Function} noCanvasSupportCallback
 *        Receives four arguments: imageURL, imageType, callbackArgs, and scale.
 *
 * @param {Function} failedLoadCallback
 *        Receives four arguments: imageURL, imageType, callbackArgs, and scale.
 *
 * @param {Function} [finallyCallback]
 *        finallyCallback is always called at the end of the process. All
 *        callbacks receive four arguments: imageURL, imageType, callbackArgs,
 *        and scale.
 *
 * @return {void}
 */
Highcharts.imageToDataUrl = function (
  imageURL,
  imageType,
  callbackArgs,
  scale,
  successCallback,
  taintedCallback,
  noCanvasSupportCallback,
  failedLoadCallback,
  finallyCallback
) {
  var img = new win.Image(),
    taintedHandler,
    loadHandler = function () {
      setTimeout(function () {
        var canvas = doc.createElement("canvas"),
          ctx = canvas.getContext && canvas.getContext("2d"),
          dataURL;
        try {
          if (!ctx) {
            noCanvasSupportCallback(imageURL, imageType, callbackArgs, scale);
          } else {
            canvas.height = img.height * scale;
            canvas.width = img.width * scale;
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
            // Now we try to get the contents of the canvas.
            try {
              dataURL = canvas.toDataURL(imageType);
              successCallback(dataURL, imageType, callbackArgs, scale);
            } catch (e) {
              taintedHandler(imageURL, imageType, callbackArgs, scale);
            }
          }
        } finally {
          if (finallyCallback) {
            finallyCallback(imageURL, imageType, callbackArgs, scale);
          }
        }
        // IE bug where image is not always ready despite calling load
        // event.
      }, loadEventDeferDelay);
    },
    // Image load failed (e.g. invalid URL)
    errorHandler = function () {
      failedLoadCallback(imageURL, imageType, callbackArgs, scale);
      if (finallyCallback) {
        finallyCallback(imageURL, imageType, callbackArgs, scale);
      }
    };
  // This is called on load if the image drawing to canvas failed with a
  // security error. We retry the drawing with crossOrigin set to Anonymous.
  taintedHandler = function () {
    img = new win.Image();
    taintedHandler = taintedCallback;
    // Must be set prior to loading image source
    img.crossOrigin = "Anonymous";
    img.onload = loadHandler;
    img.onerror = errorHandler;
    img.src = imageURL;
  };
  img.onload = loadHandler;
  img.onerror = errorHandler;
  img.src = imageURL;
};
/* eslint-enable valid-jsdoc */
/**
 * Get data URL to an image of an SVG and call download on it options object:
 *
 * - **filename:** Name of resulting downloaded file without extension. Default
 *   is `chart`.
 *
 * - **type:** File type of resulting download. Default is `image/png`.
 *
 * - **scale:** Scaling factor of downloaded image compared to source. Default
 *   is `1`.
 *
 * - **libURL:** URL pointing to location of dependency scripts to download on
 *   demand. Default is the exporting.libURL option of the global Highcharts
 *   options pointing to our server.
 *
 * @function Highcharts.downloadSVGLocal
 *
 * @param {string} svg
 * The generated SVG
 *
 * @param {Highcharts.ExportingOptions} options
 * The exporting options
 *
 * @param {Function} failCallback
 * The callback function in case of errors
 *
 * @param {Function} [successCallback]
 * The callback function in case of success
 *
 * @return {void}
 */
Highcharts.downloadSVGLocal = function (
  svg,
  options,
  failCallback,
  successCallback
) {
  var svgurl,
    blob,
    objectURLRevoke = true,
    finallyHandler,
    libURL = options.libURL || Highcharts.getOptions().exporting.libURL,
    dummySVGContainer = doc.createElement("div"),
    imageType = options.type || "image/png",
    filename =
      (options.filename || "chart") +
      "." +
      (imageType === "image/svg+xml" ? "svg" : imageType.split("/")[1]),
    scale = options.scale || 1;
  // Allow libURL to end with or without fordward slash
  libURL = libURL.slice(-1) !== "/" ? libURL + "/" : libURL;
  /* eslint-disable valid-jsdoc */
  /**
   * @private
   */
  function svgToPdf(svgElement, margin) {
    var width = svgElement.width.baseVal.value + 2 * margin,
      height = svgElement.height.baseVal.value + 2 * margin,
      pdf = new win.jsPDF("l", "pt", [width, height]); // eslint-disable-line new-cap
    // Workaround for #7090, hidden elements were drawn anyway. It comes
    // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this
    // later.
    [].forEach.call(
      svgElement.querySelectorAll('*[visibility="hidden"]'),
      function (node) {
        node.parentNode.removeChild(node);
      }
    );
    win.svg2pdf(svgElement, pdf, { removeInvalid: true });
    return pdf.output("datauristring");
  }
  /**
   * @private
   * @return {void}
   */
  function downloadPDF() {
    dummySVGContainer.innerHTML = svg;
    var textElements = dummySVGContainer.getElementsByTagName("text"),
      titleElements,
      svgData,
      // Copy style property to element from parents if it's not there.
      // Searches up hierarchy until it finds prop, or hits the chart
      // container.
      setStylePropertyFromParents = function (el, propName) {
        var curParent = el;
        while (curParent && curParent !== dummySVGContainer) {
          if (curParent.style[propName]) {
            el.style[propName] = curParent.style[propName];
            break;
          }
          curParent = curParent.parentNode;
        }
      };
    // Workaround for the text styling. Making sure it does pick up settings
    // for parent elements.
    [].forEach.call(textElements, function (el) {
      // Workaround for the text styling. making sure it does pick up the
      // root element
      ["font-family", "font-size"].forEach(function (property) {
        setStylePropertyFromParents(el, property);
      });
      el.style["font-family"] =
        el.style["font-family"] &&
        el.style["font-family"].split(" ").splice(-1);
      // Workaround for plotband with width, removing title from text
      // nodes
      titleElements = el.getElementsByTagName("title");
      [].forEach.call(titleElements, function (titleElement) {
        el.removeChild(titleElement);
      });
    });
    svgData = svgToPdf(dummySVGContainer.firstChild, 0);
    try {
      Highcharts.downloadURL(svgData, filename);
      if (successCallback) {
        successCallback();
      }
    } catch (e) {
      failCallback(e);
    }
  }
  /* eslint-enable valid-jsdoc */
  // Initiate download depending on file type
  if (imageType === "image/svg+xml") {
    // SVG download. In this case, we want to use Microsoft specific Blob if
    // available
    try {
      if (typeof nav.msSaveOrOpenBlob !== "undefined") {
        blob = new MSBlobBuilder();
        blob.append(svg);
        svgurl = blob.getBlob("image/svg+xml");
      } else {
        svgurl = Highcharts.svgToDataUrl(svg);
      }
      Highcharts.downloadURL(svgurl, filename);
      if (successCallback) {
        successCallback();
      }
    } catch (e) {
      failCallback(e);
    }
  } else if (imageType === "application/pdf") {
    if (win.jsPDF && win.svg2pdf) {
      downloadPDF();
    } else {
      // Must load pdf libraries first. // Don't destroy the object URL
      // yet since we are doing things asynchronously. A cleaner solution
      // would be nice, but this will do for now.
      objectURLRevoke = true;
      getScript(libURL + "jspdf.js", function () {
        getScript(libURL + "svg2pdf.js", function () {
          downloadPDF();
        });
      });
    }
  } else {
    // PNG/JPEG download - create bitmap from SVG
    svgurl = Highcharts.svgToDataUrl(svg);
    finallyHandler = function () {
      try {
        domurl.revokeObjectURL(svgurl);
      } catch (e) {
        // Ignore
      }
    };
    // First, try to get PNG by rendering on canvas
    Highcharts.imageToDataUrl(
      svgurl,
      imageType,
      {},
      scale,
      function (imageURL) {
        // Success
        try {
          Highcharts.downloadURL(imageURL, filename);
          if (successCallback) {
            successCallback();
          }
        } catch (e) {
          failCallback(e);
        }
      },
      function () {
        // Failed due to tainted canvas
        // Create new and untainted canvas
        var canvas = doc.createElement("canvas"),
          ctx = canvas.getContext("2d"),
          imageWidth =
            svg.match(/^<svg[^>]*width\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale,
          imageHeight =
            svg.match(/^<svg[^>]*height\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale,
          downloadWithCanVG = function () {
            ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight);
            try {
              Highcharts.downloadURL(
                nav.msSaveOrOpenBlob
                  ? canvas.msToBlob()
                  : canvas.toDataURL(imageType),
                filename
              );
              if (successCallback) {
                successCallback();
              }
            } catch (e) {
              failCallback(e);
            } finally {
              finallyHandler();
            }
          };
        canvas.width = imageWidth;
        canvas.height = imageHeight;
        if (win.canvg) {
          // Use preloaded canvg
          downloadWithCanVG();
        } else {
          // Must load canVG first. // Don't destroy the object URL
          // yet since we are doing things asynchronously. A cleaner
          // solution would be nice, but this will do for now.
          objectURLRevoke = true;
          // Get RGBColor.js first, then canvg
          getScript(libURL + "rgbcolor.js", function () {
            getScript(libURL + "canvg.js", function () {
              downloadWithCanVG();
            });
          });
        }
      },
      // No canvas support
      failCallback,
      // Failed to load image
      failCallback,
      // Finally
      function () {
        if (objectURLRevoke) {
          finallyHandler();
        }
      }
    );
  }
};
/* eslint-disable valid-jsdoc */
/**
 * Get SVG of chart prepared for client side export. This converts embedded
 * images in the SVG to data URIs. It requires the regular exporting module. The
 * options and chartOptions arguments are passed to the getSVGForExport
 * function.
 *
 * @private
 * @function Highcharts.Chart#getSVGForLocalExport
 * @param {Highcharts.ExportingOptions} options
 * @param {Highcharts.Options} chartOptions
 * @param {Function} failCallback
 * @param {Function} successCallback
 * @return {void}
 */
Highcharts.Chart.prototype.getSVGForLocalExport = function (
  options,
  chartOptions,
  failCallback,
  successCallback
) {
  var chart = this,
    images,
    imagesEmbedded = 0,
    chartCopyContainer,
    chartCopyOptions,
    el,
    i,
    l,
    href,
    // After grabbing the SVG of the chart's copy container we need to do
    // sanitation on the SVG
    sanitize = function (svg) {
      return chart.sanitizeSVG(svg, chartCopyOptions);
    },
    // When done with last image we have our SVG
    checkDone = function () {
      if (imagesEmbedded === images.length) {
        successCallback(sanitize(chartCopyContainer.innerHTML));
      }
    },
    // Success handler, we converted image to base64!
    embeddedSuccess = function (imageURL, imageType, callbackArgs) {
      ++imagesEmbedded;
      // Change image href in chart copy
      callbackArgs.imageElement.setAttributeNS(
        "http://www.w3.org/1999/xlink",
        "href",
        imageURL
      );
      checkDone();
    };
  // Hook into getSVG to get a copy of the chart copy's container (#8273)
  chart.unbindGetSVG = addEvent(chart, "getSVG", function (e) {
    chartCopyOptions = e.chartCopy.options;
    chartCopyContainer = e.chartCopy.container.cloneNode(true);
  });
  // Trigger hook to get chart copy
  chart.getSVGForExport(options, chartOptions);
  images = chartCopyContainer.getElementsByTagName("image");
  try {
    // If there are no images to embed, the SVG is okay now.
    if (!images.length) {
      // Use SVG of chart copy
      successCallback(sanitize(chartCopyContainer.innerHTML));
      return;
    }
    // Go through the images we want to embed
    for (i = 0, l = images.length; i < l; ++i) {
      el = images[i];
      href = el.getAttributeNS("http://www.w3.org/1999/xlink", "href");
      if (href) {
        Highcharts.imageToDataUrl(
          href,
          "image/png",
          { imageElement: el },
          options.scale,
          embeddedSuccess,
          // Tainted canvas
          failCallback,
          // No canvas support
          failCallback,
          // Failed to load source
          failCallback
        );
        // Hidden, boosted series have blank href (#10243)
      } else {
        ++imagesEmbedded;
        el.parentNode.removeChild(el);
        checkDone();
      }
    }
  } catch (e) {
    failCallback(e);
  }
  // Clean up
  chart.unbindGetSVG();
};
/* eslint-enable valid-jsdoc */
/**
 * Exporting and offline-exporting modules required. Export a chart to an image
 * locally in the user's browser.
 *
 * @function Highcharts.Chart#exportChartLocal
 *
 * @param  {Highcharts.ExportingOptions} [exportingOptions]
 *         Exporting options, the same as in
 *         {@link Highcharts.Chart#exportChart}.
 *
 * @param  {Highcharts.Options} [chartOptions]
 *         Additional chart options for the exported chart. For example a
 *         different background color can be added here, or `dataLabels`
 *         for export only.
 *
 * @return {void}
 *
 * @requires modules/exporting
 */
Highcharts.Chart.prototype.exportChartLocal = function (
  exportingOptions,
  chartOptions
) {
  var chart = this,
    options = Highcharts.merge(chart.options.exporting, exportingOptions),
    fallbackToExportServer = function (err) {
      if (options.fallbackToExportServer === false) {
        if (options.error) {
          options.error(options, err);
        } else {
          Highcharts.error(28, true); // Fallback disabled
        }
      } else {
        chart.exportChart(options);
      }
    },
    svgSuccess = function (svg) {
      // If SVG contains foreignObjects all exports except SVG will fail,
      // as both CanVG and svg2pdf choke on this. Gracefully fall back.
      if (
        svg.indexOf("<foreignObject") > -1 &&
        options.type !== "image/svg+xml"
      ) {
        fallbackToExportServer(
          "Image type not supported" + "for charts with embedded HTML"
        );
      } else {
        Highcharts.downloadSVGLocal(
          svg,
          extend({ filename: chart.getFilename() }, options),
          fallbackToExportServer
        );
      }
    },
    // Return true if the SVG contains images with external data. With the
    // boost module there are `image` elements with encoded PNGs, these are
    // supported by svg2pdf and should pass (#10243).
    hasExternalImages = function () {
      return [].some.call(
        chart.container.getElementsByTagName("image"),
        function (image) {
          var href = image.getAttribute("href");
          return href !== "" && href.indexOf("data:") !== 0;
        }
      );
    };
  // If we are on IE and in styled mode, add a whitelist to the renderer for
  // inline styles that we want to pass through. There are so many styles by
  // default in IE that we don't want to blacklist them all.
  if (isMSBrowser && chart.styledMode) {
    Highcharts.SVGRenderer.prototype.inlineWhitelist = [
      /^blockSize/,
      /^border/,
      /^caretColor/,
      /^color/,
      /^columnRule/,
      /^columnRuleColor/,
      /^cssFloat/,
      /^cursor/,
      /^fill$/,
      /^fillOpacity/,
      /^font/,
      /^inlineSize/,
      /^length/,
      /^lineHeight/,
      /^opacity/,
      /^outline/,
      /^parentRule/,
      /^rx$/,
      /^ry$/,
      /^stroke/,
      /^textAlign/,
      /^textAnchor/,
      /^textDecoration/,
      /^transform/,
      /^vectorEffect/,
      /^visibility/,
      /^x$/,
      /^y$/,
    ];
  }
  // Always fall back on:
  // - MS browsers: Embedded images JPEG/PNG, or any PDF
  // - Embedded images and PDF
  if (
    (isMSBrowser &&
      (options.type === "application/pdf" ||
        (chart.container.getElementsByTagName("image").length &&
          options.type !== "image/svg+xml"))) ||
    (options.type === "application/pdf" && hasExternalImages())
  ) {
    fallbackToExportServer("Image type not supported for this chart/browser.");
    return;
  }
  chart.getSVGForLocalExport(
    options,
    chartOptions,
    fallbackToExportServer,
    svgSuccess
  );
};
// Extend the default options to use the local exporter logic
merge(true, Highcharts.getOptions().exporting, {
  libURL: "https://code.highcharts.com/8.0.0/lib/",
  // When offline-exporting is loaded, redefine the menu item definitions
  // related to download.
  menuItemDefinitions: {
    downloadPNG: {
      textKey: "downloadPNG",
      onclick: function () {
        this.exportChartLocal();
      },
    },
    downloadJPEG: {
      textKey: "downloadJPEG",
      onclick: function () {
        this.exportChartLocal({
          type: "image/jpeg",
        });
      },
    },
    downloadSVG: {
      textKey: "downloadSVG",
      onclick: function () {
        this.exportChartLocal({
          type: "image/svg+xml",
        });
      },
    },
    downloadPDF: {
      textKey: "downloadPDF",
      onclick: function () {
        this.exportChartLocal({
          type: "application/pdf",
        });
      },
    },
  },
});
